Skip to content

fix(confinement,scanners): case-folding confinement, MCP launcher/DNS pinning, bounded SSE & secret redaction - #140

Merged
aisecnomad merged 160 commits into
mainfrom
fix/audit-pr2-confinement-and-scanners
Nov 12, 2026
Merged

aisecnomad merged 160 commits into
mainfrom
fix/audit-pr2-confinement-and-scanners

Conversation

@aisecnomad

Copy link
Copy Markdown
Collaborator

Summary

Remediates the Filesystem Confinement, MCP Scanners, Secret Redaction & Parser Safety findings from the end-to-end security & architecture audit (NS-HIGH-04, scanner side of NS-MED-01, NS-MED-05, parser/stream items of NS-MED-07, and Policy schema strictness in NS-LOW-01).

This PR is 2 of 4 strictly file-disjoint subsystem PRs targeting claude/audit-remediation (0 overlapping paths across PRs 1–4; merges cleanly in any order).

Findings Remediated

ID Severity Finding & Fix
NS-HIGH-04 High Case-Insensitive Filesystem Confinement & Symlink Escape Defense: Hardened utils/confined.py (open_confined, iter_confined_files, ConfinedCollection) with dir_fd-anchored scandir exact-casing verification on case-insensitive filesystems (macOS APFS, Windows NTFS) so case-folded path variants or symlink swaps cannot bypass path-identity checks or duplicate/alias component identities.
NS-MED-01 Medium MCP Launcher Pinning & Wrapper Bypass Hardening (Scanner Side): Expanded scanners/mcp.py and scanners/mcp_launch.py wrapper/interpreter unwrapping (env, npx, uvx, pipx, bunx, pnpx, poetry, pdm, hatch, rye, mise, asdf, volta, corepack, deno, docker/podman run) and flagged mutable tags (@latest, unpinned registry runners, floating container tags) as unpinned execution surfaces.
NS-MED-05 Medium Live MCP DNS/Redirect Rebinding & Bounded SSE Stream Parsing: Pinned resolved peer IP addresses across connection validation in scanners/live_mcp.py, disabled cross-origin/rebinding redirects, and enforced strict byte/event/line budgets on text/event-stream (SSE) responses so malicious MCP endpoints cannot stall or OOM live discovery.
NS-MED-07 Medium Secret Redaction Coverage, JSONC Comment Stripping & Pickle Opcode Bounds: Expanded utils/secrets.py credential pattern redaction (GitHub fine-grained PATs, Anthropic/OpenAI/HuggingFace/Slack/AWS session tokens, Bearer/Basic auth headers, URL userinfo, PEM blocks), hardened utils/jsonc.py trailing-comma and string-literal comment state machine, and bounded utils/model_formats.py pickle/safetensors header scanning.
NS-LOW-01 Low Policy Model Unknown-Field Rejection: Configured Policy schemas in policy.py with extra="forbid" so misspelled policy keys fail closed during validation rather than silently dropping enforcement rules.

Touched Files (13 exclusive paths)

  • src/nexus_symbiote/policy.py
  • src/nexus_symbiote/scanners/live_mcp.py
  • src/nexus_symbiote/scanners/mcp.py
  • src/nexus_symbiote/scanners/mcp_launch.py
  • src/nexus_symbiote/utils/confined.py
  • src/nexus_symbiote/utils/jsonc.py
  • src/nexus_symbiote/utils/model_formats.py
  • src/nexus_symbiote/utils/secrets.py
  • tests/test_audit_pr2_confinement_scanners.py
  • tests/test_confined_links_and_variants.py
  • tests/test_prompt_imports.py
  • tests/test_prompt_surfaces.py
  • tests/test_release_due_diligence.py

Verification

  • ruff check src tests scripts & ruff format --check src tests scripts — passed
  • mypy src/nexus_symbiote scripts — 0 issues in 88 source files
  • scripts/check_security_regressions.py — 18 cases, 0 failures
  • scripts/check_repository_hygiene.py — passed
  • pytest across 18 confinement, MCP, live-wire, prompt, and PR 2 regression suites — 1,120 passed, 0 failed

…ent boundary

Trust boundary: the descriptor-relative confined reader between the scanned
checkout and every collector.

A fixed-path probe that found no file was silent, so a checkout carrying
claude.md, .Mcp.json or .claude/Settings.json had nothing inventoried on the
case-sensitive scan host while case-insensitive developer machines load
exactly those files. A missing fixed path now records a failure for each
existing case or normalisation variant, listed descriptor-relatively.

Any symlink failed the scan, so ordinary layouts such as CLAUDE.md ->
AGENTS.md had no passing configuration. A relative symlink whose target,
resolved lexically against the link's own directory, is a regular file or
directory beneath the root, not excluded and reached without a further link,
is now read through its target; every opened component still uses
O_NOFOLLOW and the link is re-resolved after the read. Walks yield in-root
file links under their own name and skip in-root directory links, whose
contents are walked under the target's name. Absolute, escaping, dangling,
chained, excluded-target and special-file links stay failures with a named
reason. Integrity checks of an in-root link hash its target the same way.

parse_python turns the parser's MemoryError and RecursionError into a
per-file ValueError, so one deeply nested file can no longer discard a whole
collector's inventory, and accepts a UTF-8 byte-order mark as CPython does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
… signatures

Trust boundary: A2A agent cards, client configuration and SDK source in the
scanned checkout, which the a2a collector parses without contacting agents.

One OAuth scope of a few megabytes made the token-shape regex backtrack
quadratically and held a scan for hours; scopes over 512 characters are now
withheld unmatched and both alternatives start only at a word boundary.

Cards under public/.well-known or in a package directory were not read and
left no gap. Cards are now found in any .well-known directory and at the
root paths, case variants of those paths fail, identical cards published
under both well-known names merge into one record, and a card built in
Python code is a coverage gap. Python SDK references no longer fail the
collector: they are read under their own budget, an unreadable file is no
gap, and adding an unrelated file changes nothing in the inventory.

Signatures (count, alg, kid), capability flags, extensions and the provider
origin are recorded without claiming verification and enter the structure
digest (profile a2a-card-structure/v3), so stripping a signature or enabling
push notifications alerts without the binding key. Schemes typed "none" or
unknown, anonymous legacy names, and a legacy authentication object on a
card that uses the 0.3 or 1.0 declarations no longer count as declared
authentication. Every client endpoint field gets the transport check, both
client list names are read, names are bounded and neutralised in findings,
and records carry root-relative paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: RAG configuration files and Python sources in the scanned
checkout, which the rag collector reads into exported inventories.

Source URIs were exported as origins with no digest or binding, and controls
only by truthiness, so repointing a source within one host, changing an
index or setting "sanitization": "off" passed scan, validate and diff. Each
source now carries a structure digest (profile rag-source-structure/v1) over
its field names, exported identities, URI shapes, inline-document digest and
control states, an HMAC over the whole source under
NEXUS_SYMBIOTE_SOURCE_BINDING_KEY (rag-source-values/v1), and a low
completeness risk naming the key when values are unbound. Only a boolean
true, or an object whose enabled is true, declares a control.

Inline documents were copied into the BOM as source URIs; they are now
counted and digested, never exported. Connection strings, user:secret@host
forms, token-shaped values and URLs with a backslash authority are exported
as redaction markers, and store types and other identities only when they
are plain identifiers, so source_credentials_omitted is true by
construction. The transport check reads the stripped URI's scheme and also
flags ws, git, redis and other clear-text schemes.

Parsing every Python file no longer fails the collector or the gates: the
import heuristic reads under its own budget, skips files it cannot parse and
lists one constant surface instead of every file. A data file named
knowledge_base.json is not taken for a configuration, unnamed sources are
identified by their file, both source list names are read, and paths are
root-relative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…h full hidden-character checks

Trust boundary: instruction, rule, skill and command files in the scanned
checkout that agent hosts pass to the model.

A .clinerules directory failed the collector, and .cursor/rules,
.github/instructions, .github/prompts, .windsurf/rules, .claude/rules,
.claude/agents, GEMINI.md, CLAUDE.local.md and skill scripts were neither
inventoried nor checked. Every file in those locations is now recorded by
digest, and hidden-character detection runs on every UTF-8 file.

A CLAUDE.md that imports @docs/policy.md kept its digest while the imported
instructions changed. In-root @path imports of Claude memory files and
GEMINI.md are now followed to five hops through the confined reader and
inventoried, and imports outside the root are coverage gaps.

Detection now covers every format character, variation selectors, U+180E,
Hangul fillers and invisible operators, while emoji joiner sequences,
presentation selectors after pictographs, the three recommended subdivision
flags and a leading byte-order mark are no longer reported as high or
critical; directional marks are medium. HTML and Markdown comments, hidden
when rendered, are a low finding that reports only their count and length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: the confined reader's fixed-path probes through an in-root
directory symlink.

With .claude linked to another directory of the checkout, probing the
absent .claude/settings.local.json was reported as a dangling link, so an
ordinary layout failed collection. A file missing below a linked directory
that exists is now absent, as it would be without the link; a link whose
own target directory is missing stays a failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: IDE agent settings, plugin manifests and marker files in the
scanned checkout, which hosts apply to every contributor who opens it.

From .claude/settings.json the inventory kept only the permission mode and
whether the allow list held one of seven literal grants, so adding a hook
command, removing a deny rule, adding apiKeyHelper, a plugin marketplace or
enableAllProjectMcpServers, or respelling a grant as Bash(**), Edit(//**) or
mcp__server changed nothing. Each settings file is now bound by a structure
digest over every key and redacted value (claude-settings-structure/v1,
environment values by name only) and, with
NEXUS_SYMBIOTE_SOURCE_BINDING_KEY, an HMAC over the whole files; findings
cover hook commands, MCP auto-approval, credential helpers, plugins and
marketplaces, additional directories, the bypassPermissions and acceptEdits
modes and blanket grants parsed from the rule syntax. A plugin repository's
own manifests and hooks are bound the same way, and settings nested in a
subdirectory, which a host started there applies, are separate records.

Markers and settings are read at fixed paths, so case variants are reported
and in-root links followed. Codex config.toml approval and sandbox settings
are read (its MCP servers are a coverage gap), Gemini CLI is recognised, and
.vscode/extensions.json is read as VS Code reads it, with comments and
trailing commas but no duplicate keys. Paths are root-relative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: a native inventory is input from whoever produced it (a
supplier, an earlier or later build, a pull request), and every reading
command parses it: validate, risk, diff, summary, vex, blast-radius and
baseline.

Abuse cases addressed:
- A document from a newer build, or one declaring an unknown format
  version, failed with the same opaque message as a corrupt file.
  spec_version is now checked first (this build reads 1.0.x and 1.1.x)
  and refused with a message that names the supported versions and never
  echoes document text. New inventories are 1.1.0, which marks
  scan-root-relative paths.
- Dependency edges silently dropped unknown keys, so a misspelt
  "condition" became an unconditional relationship. Edges now forbid extra
  fields, and booleans, integers and timestamps are as strict as the
  published schema ("no", 0, "3" and numeric timestamps are refused).
- AIBOM.load followed symbolic links and blocked on a FIFO, and
  AIBOM.save wrote through a link, non-atomically. Both now use the
  project's no-follow, regular-file and atomic-write primitives.
- scan and import-shadowscan could write an inventory larger than the
  16 MiB every reader accepts. One limit now governs writer and reader:
  oversized output is written compactly or refused with the limit named,
  load errors name the class of problem without quoting the document, and
  baseline reports a staged-load failure without a traceback.
- bom_ref and edge endpoints are limited to 16,384 characters, so one
  reference cannot be repeated into every validation message.

The published schema is regenerated from the model, carries $schema and
$id, and marks the types and fields no collector populates as reserved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: validate and risk judge an inventory someone else
supplied, and scan, validate and risk honour a policy file from the
checkout unless --no-project-policy is passed. Neither the inventory nor
that policy may turn a gate green without evidence from the reviewer's
checkout, or relax a gate without the relaxation being recorded.

Abuse cases addressed:
- Integrity counted a tool descriptor that merely agreed with its own
  recorded digest as a verified hash, so a hand-written inventory, or one
  supplementary served-descriptor record added to an honest scan, turned
  integrity from unknown to PASS. Only a descriptor matching the tool
  collected from the project's MCP configuration now counts; anything else
  is unknown, and a run that verified nothing says so.
- Project reconciliation compared components only: a supplier could
  delete or forge relationships, rewrite the repository agent's autonomy,
  references or authorizations, or add components claiming verified
  assurance, under a "project-reconciled" label. Relationships between
  local components and the repository agent (matched by identity across
  checkout paths) are now compared; components or edges asserting
  verified assurance or internal-verified trust without local evidence
  fail; other supplier additions are listed as supplier claims under the
  basis project-reconciled-with-supplier-claims. validate --project, like
  risk --project, needs at least one local subject.
- A supplied inventory could make validate re-read one large file once
  per component and per case-variant hash key, or build one message per
  dangling reference that repeats a megabytes-long reference. Digests are
  cached per file identity and algorithm, every artifact read (including
  model inspection) is charged to a 30 GiB budget beyond which evidence is
  unknown, hash keys are case-folded, and issue lists are bounded.
- The discovery producer check passed vacuously when an inventory dropped
  every ShadowScan marker while the signed report was still present; it
  now fails when a report lies beneath the project or the operator
  signature policy names one. --require-discovery-signature lets CI that
  ignores the project policy require the signer, and an unverified
  producer is reported instead of passing silently.
- Imported ShadowScan model and tool findings could never pass integrity.
  Claims bound to the local report by reconciliation are judged as
  imported claims; intrinsic findings such as an unsafe format still fail.
- One false positive forced a category-wide ignore_categories or a
  disabled gate. risk.suppressions accepts exactly one category on one
  component with a reason, owner and expiry; an expired entry fails the
  gate, and every entry is listed in policy_weakened, in risk JSON and,
  through scan, in the inventory metadata with the governing policy.
- policy_weakened omitted a raised warn_threshold and extra exclude_paths,
  the policy file was located twice, and a .yaml file could silently
  shadow a stricter .yml.

validate, risk and drift now share one typed coverage evaluation
(core/coverage.py); scan and reconciliation share one merge rule
(core/collection.py), so identical duplicates from two collectors are
kept once and components measured from bytes are recorded as observed.
Every failing gate carries a reason in its report object, and risk JSON
records the effective gate parameters with their source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: operators decide from the documentation which results to
trust and which settings a checkout may change; a policy key that is
undocumented, or a guarantee stated more broadly than the code enforces,
leads to gates that are weaker than they appear.

- docs/policy-reference.md describes every .nexus-symbiote.yaml key, its
  default and effect on each gate, the new suppressions, what
  policy_weakened reports, where the governing policy is recorded and
  what is not supported; the quoted error messages were reproduced from
  this tree. It is linked from the documentation hub.
- docs/security-model.md, docs/shadowscan-integration.md and
  docs/verification-source-binding.md state the wider reconciliation
  scope, what component assurance means, how imported claims are judged,
  the producer check for stripped inventories, and that a tool descriptor
  counts only when it matches the project's configuration.
- SECURITY.md names the operator-side --require-discovery-signature flag,
  notes that the project setting is ignored under --no-project-policy, and
  states the validation work budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…r blast radius

Trust boundary: agent.yaml is checkout-controlled input that the repository
assembler reads on every scan, and blast-radius is the operator's answer to
"what does this compromised component reach".

Abuse cases closed:
- A declared local model compared a root-relative manifest path with the
  absolute path collectors record, so every such scan aborted with no
  inventory; a non-conforming or foreign agent.yaml aborted the same way.
  Manifest problems are now recorded as scan failures and the inventory is
  still written; validate --project stays strict.
- The manifest ignored the operator's exclude_paths, so a pull request could
  name an excluded file (for example secrets/service.env) and have its
  unsalted digest exported. Exclusions now bind the manifest and every path
  it names, recorded as a coverage gap.
- Declared instructions were inventoried twice and manifest-only ones were
  never checked for hidden characters; they now map to the prompt
  collector's record (digest must match) or are checked like it. YAML
  anchors, aliases, tags and lone surrogates are refused.
- Registry packages and container images were graph orphans, so
  blast-radius reported no affected agent for a compromised dependency.
  They are linked to the repository agent (depends-on), MCP servers to the
  package they launch (registered-as), and blast-radius reports dependents,
  says when a component is unlinked, and runs one traversal per direction
  instead of one per agent. Graph issue messages are bounded.
- Assembly looked up each tool's server with a linear scan; it now uses an
  index.
- The path-inferred agent's reference hashed the absolute checkout path; it
  is now agent:repository:inferred wherever the tree is checked out.

baseline gains --no-project-policy and --signature-policy so the reviewed
snapshot and the candidate can be taken under the same policy. The manifest
is documented in docs/agent-manifest.md with a published JSON Schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…ucer

Trust boundary: the native writer and the human-readable risk report are
what an operator sees of an inventory produced elsewhere.

- to_native_json built the compact serialization of every inventory even
  when the indented one fit the 16 MiB limit, doubling the cost of every
  scan and save; the compact form is now produced only for oversized
  inventories.
- Human-readable risk did not say when ShadowScan evidence was accepted
  without a producer check, although the JSON output did; it now prints
  "Discovery producer: not verified (unsigned evidence accepted)", so an
  unauthenticated discovery report is not mistaken for a verified one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…elationships

Trust boundary: diff --fail-on-alert is the CI gate between a reviewed
snapshot and the change under review; whatever it rates info passes.

Abuse cases closed:
- A removed component was rated info and never alerted, so replacing a
  reviewed dependency or base image with one the collector does not record
  (open-ai for openai, an attacker image for a pinned ollama image), or
  deleting a guardrail requirement, passed the gate. Removals now alert:
  high for MCP servers and clients, tools, models, instruction and skill
  files, guardrails, IDE and A2A configuration and any record that carried
  integrity evidence, medium otherwise.
- Coverage was compared as one blob with one constant high alert, so an
  added Python file failed the gate exactly like a failed collector and
  trained teams to re-baseline without review. Coverage is now compared
  field by field: a new failure, gap or exclusion, a worse collector status,
  fewer collectors, a scope change or a ShadowScan regression alerts high
  and names itself (as a multiset, so a second identical gap is new); a
  changed list of scanned surfaces and improvements are info; gaps both
  snapshots share are reported as a note.
- Added and removed relationships were unrated and invisible in text
  output; they are rated (high for new invokes, delegates-to and
  acts-as-identity, removed uses-guardrail and approved-under) and printed.
- Trust-tier and signing-status upgrades in a supplied BOM passed as info;
  they are medium. Added risk findings carry their own severity, and
  agent_card_url, provider, source_uri, signing_key_reference and the
  declared autonomy level rate high. Opaque values compare as JSON, so true
  is no longer equal to 1.
- A supplied BOM with dangling references, an unsupported spec_version or
  swapped arguments compared clean; new graph problems and unsupported
  versions alert, and a reversed order is noted.
- Inserting one ShadowScan finding, or reordering CycloneDX dependencies,
  raised an alert for every later component or edge; positional indexes no
  longer take part in the comparison.

A new Nexus SymBiote release is an info note rather than a medium alert.
docs/drift-gate.md documents where the reviewed baseline must live and what
each alert means.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…nized values

Trust boundary: a ShadowScan report found in the checkout is untrusted,
unsigned-by-default discovery evidence that the scan and every gate import.

Abuse cases closed:
- The collector imported the first of .shadowscan/report.json,
  shadowscan-report.json and shadowscan.json and never looked at the others,
  so a committed copy at a higher-precedence name silently replaced the
  report the pipeline had just generated, hiding a new critical agent from
  scan, validate and risk. More than one report is now a scan failure that
  names the paths and imports neither; the inventory records the path it
  read under surfaces_scanned.
- A risk level, shadow flag or surface the projection does not recognize
  (Critical instead of critical, "true" instead of true) was dropped while
  the finding counted as mapped and the source as complete. Such values are
  now projection limitations and a coverage gap, and contradictory CycloneDX
  type, group and kind combinations stay unmapped (projection profile v4).
  The summary's by_risk_level and shadow counts are cross-checked.
- json.loads accepted UTF-16 and UTF-32 reports that verify-discovery cannot
  read; reports must be UTF-8 (an optional byte-order mark is accepted).
- SARIF and VEX could present unauthenticated imported heuristics as this
  tool's own findings (the Action's SARIF upload and the vex command had no
  check). Both now omit imported findings and record how many they omitted.

The integration guide's opening flow no longer ends in a command that fails
for every repository, and the report paths, limits and rerun caveats are
documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…ursion

Trust boundary: LineageIndex decides whether a principal may retrieve a
memory, cache or index version, given its ancestry and ACL revisions.

Abuse cases closed:
- An agent that can append to a lineage could make every later retrieval
  raise RecursionError after about 1,000 generations, and rebuilding the
  index was quadratic in its depth. Construction now uses Kahn's algorithm,
  withdrawal uses a children index, and the ancestry check is iterative
  with one visited set per retrieval.
- update_acl accepted any revision string, so an earlier, wider ACL could be
  reinstated under its old revision and replay retrievals approved against
  it. A revision already used for a version is now refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: the README recipe is what most CI pipelines copy; a gate that
reads its baseline from the pull request it reviews is self-attestation.

The README drift recipe now takes the baseline under the same policy as the
candidate, says that removals and coverage regressions fail the gate while
an added source file does not, and links the guide that explains where a
reviewed baseline must live. The collector table points to the optional
agent.yaml manifest, the documentation hub lists both new pages, and the
SECURITY.md limitation on instruction files outside the fixed paths now
says a manifest can bring them into the inventory and the hidden-character
check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…olicy

Trust boundary: model files, their configuration and everything beside them
come from the scanned checkout and are hostile input. Only the operator's
signature policy, kept outside the checkout, can establish their integrity.

Abuse cases closed:
- A pull request swaps an ONNX external-data file, a SavedModel variables
  file or one shard of a sharded checkpoint. The inventory recorded only the
  graph file, so diff and validate stayed silent. Bound weight files are now
  hashed and recorded per model, and validate --project observes them again.
- A model directory declares auto_map custom code, sets trust_remote_code in
  a config behind an empty config.json, or edits the chat template in
  tokenizer_config.json. None of it changed the inventory. Every config,
  tokenizer file, chat template and declared Python module is now digested,
  and declared custom code is a high finding.
- Model references used the checkout directory's name and absolute paths,
  so same-named models collided and an inventory made elsewhere could not be
  reconciled. References and recorded paths are now root-relative.
- An older artifact signed by the same identity verified under the signer
  policy. Policy version 2 adds expected_sha256 and operator digest pins;
  every weight file must be covered, the verification evidence is recorded
  on the component, and policy entries that nothing verifies, or a policy
  stored inside the project or hard-linked, fail.
- Renamed pickles, text-protocol pickles and torch ZIP checkpoints behind
  other model suffixes are rated code-executing, an unreadable pickle keeps
  that rating, and UTF-16 SafeTensors headers are no longer safe. One
  SafeTensors validator serves the scan and validate.

OpenSSF Model Signing (DSSE) bundles are refused with an explicit reason;
that format is not verified yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: operators decide what to trust from these pages, so they
must say which model files the inventory binds, which policy entries
establish integrity, and what stays unverified.

Abuse case: an operator follows the old advice to sign with OpenSSF Model
Signing, or keeps the signer policy in the repository, and believes the
models are verified while nothing checks them. The new model integrity page
states what is bound, the version 1 and 2 policy formats, how to produce a
signature the tool verifies, the evidence it records and what it does not
establish. SECURITY.md lists the remaining model limitations, and the
migration guide explains the changed model references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: a GGUF file's metadata keys are bytes chosen by whoever
wrote the file, and the template key names reach the inventory as the
source of each digested chat template.

Abuse case: a key such as "tokenizer.chat_template.<ESC>[31m..." carried
terminal control characters into the BOM, where any consumer that prints
the source verbatim would render them. Names outside letters, digits, "_"
and "-" are now recorded by position instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: everything a collector reads from the scanned checkout is
untrusted; how much it may read is decided by the operator alone.

Collection bounds were constants: 20,000 directory entries, 4 MiB per file,
32 MiB per collector, 5 GiB for model weights and 4 MiB for tokenizers.
Ordinary checkouts and model directories crossed them, the failure did not
say which bound was hit, and nothing could raise one, so the only way past
was report-only mode, which switches the gates off.

src/nexus_symbiote/limits.py defines the named limits, their defaults and
hard maximums. They are set only with the root option --limit NAME=VALUE
or NEXUS_SYMBIOTE_LIMITS, the option winning per name; a malformed setting
is a usage error whose message never repeats what was supplied. Nothing in
the checkout, its policy file included, can change a limit, and every limit
still fails closed: reaching one records a scan failure that names the
limit, its value, the root-relative path and how to raise it, without file
content. Raising a limit moves where collection stops; it cannot hide a
stop or pass a gate, and lowering one only adds failures.

The confined reader also charges the root listing to the entry budget,
reports an exhausted budget once, skips and reports names that are not
UTF-8 instead of writing an inventory no command can load, refuses JSON
numbers that overflow to infinity, checks policy exclusions in O(depth)
instead of entries times files (20,000 exclusions made a 200-file walk take
67 s), and walks ".." in an operator path through the real parent, so
"link/../x" fails closed instead of naming another file. validate and
verify-artifact resolve --project like scan.

Defaults raised where ordinary projects exceed them: entries 100,000;
model-bytes 64 GiB, now also for tokenizers; model-total-bytes 256 GiB.
docs/collection-limits.md lists every limit, its default, what happens
when it is reached and how to raise it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…r reads

Trust boundary: everything under the scanned checkout is attacker-controlled
input to the registry collector, including manifests a pull request adds or
edits. The inventory, its coverage claim and the drift gate must not be steered
by how a declaration is spelled, which file a package manager prefers, or how
expensive a file is to parse.

Abuse cases this closes:
- Spelling and scope evasion: names were matched before PEP 503 normalisation
  and only after a "/", so llama.index.core, mcp.server.fetch, mcp, fastmcp,
  @anthropic-ai/sdk, @openai/agents and @langchain/core were never inventoried
  while coverage read complete. Names are now normalised first and matched
  against one documented catalog (names, prefixes, words, npm scopes); an npm
  alias is classified and recorded by the package it installs.
- Precedence and override files: npm-shrinkwrap.json is now the lock source
  when present (never replaced by package-lock.json); npm overrides, yarn
  resolutions, pnpm.overrides, workspaces, [tool.uv.sources], uv
  override/constraint dependencies, PDM overrides and a Pipfile are coverage
  gaps; peerDependencies, PEP 735 dependency groups, build requirements and uv
  and PDM development dependencies are read; all eight default Compose file
  names, Containerfile and Compose models are read.
- Lock records taken at face value: a resolved URL with a backslash, empty
  host, non-https scheme, control or non-ASCII character is a failure; missing
  or SHA-1-only integrity, another registry and a tarball naming another
  package or version are findings; an exact spec the lock contradicts is a
  failure; package URLs from another registry carry repository_url.
- Compose read by regex: files are composed as YAML nodes (aliases shared,
  merge keys followed once per mapping within a step budget), so quotes, flow
  mappings and block scalars are read correctly and comments or environment
  values never become components; one image in several files is one component.
- Ratings hidden by order: a URL declaration behind a pin is a finding and
  removes the PyPI package URL; marker-only pins are low; the rating no longer
  depends on which file is read first; --hash values are recorded for drift.
- Denial of service: the Dockerfile directive and heredoc patterns backtracked
  quadratically (hours at 4 MiB); they are replaced by linear readers proved
  equal on exhaustive inputs. pyproject.toml is bounded (256 KiB, 16-dot keys)
  before tomllib, quoted option text over 16 KiB is not given to shlex, and a
  deeply nested marker fails its own line instead of the whole collector.
- Identity: images record their registry type, host and normalised repository
  without redaction collapsing tags, digest-pinned images get pkg:oci URLs, and
  package identifiers with an unknown purl type or qualifier are not projected.
  Every path the collector records is relative to the scan root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: an MCP server's served declarations (tool descriptors,
initialize instructions, prompts and resource templates) are untrusted
input that can change on the server without any change to the repository.

Abuse case: a remote or pinned stdio server rewrites a tool description,
adds a tool, or keeps tools/list byte-identical and changes its initialize
instructions to carry injected directions. discover-mcp output could not
enter an inventory or diff, verify-live failed for any inventory that also
held a remote server and refused title, outputSchema and _meta, and no path
observed instructions, prompts or resource templates, so these rug pulls
raised no alert.

- discover-mcp --record writes a native inventory of a complete
  observation: observed server and tool components, negotiated protocol,
  server identity, capabilities, redacted instructions, prompts, resource
  templates and a server declaration digest. diff rates a changed tool
  definition, served tool set or declaration digest as critical.
- hash_tool_definition binds title, outputSchema and _meta when present;
  every fingerprint without them is unchanged. Static configuration, stdio
  and HTTP intake share the profile, and known-answer vectors pin it.
- verify-live reports servers reached over HTTP as not verifiable instead
  of failing the run, accepts the current descriptor fields, compares the
  server declaration, gives each failure a reason code, records input
  digests, requires --project, refuses a launch policy inside the
  repository of the project or the inventory, with more than one link or
  writable by others, and --record exits 1 on drift unless --accept-drift.
- The client negotiates MCP 2025-11-25, 2025-06-18, 2025-03-26 or
  2024-11-05, decodes chunked SSE through http.client with the WHATWG line
  rules, bounds served schemas before the JSON Schema check and records any
  check failure, refuses special-purpose addresses from an explicit list,
  ignores SSLKEYLOGFILE and lets the operator set clientInfo and User-Agent.
- The pinned stdio session skips notifications and reports a failure after
  tools/call as ToolCallError or ExecutionOutcomeUnknown, never as a denial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: OSV replies are untrusted network input, and the package
names and versions sent to OSV leave the operator's machine.

Abuse case: advisories exited 0 when it found advisories and reported
complete: true when nothing was checked, so a CI step passed known
vulnerabilities; one malformed reply ended the run with a traceback and
discarded every earlier result; private package names were sent without
the documentation saying so or an option to exclude them; pinned MCP
launcher packages were never queried.

- Exit 1 when an advisory is found (--no-fail-on-advisory reports only) or
  when coverage is partial or none, including an inventory with no exact
  PyPI or npm package. The report states coverage, checked_at, the tool
  version, the inventory digest, skipped and excluded components, the
  exact queries and what was sent.
- Query /v1/querybatch and follow each query's pages under a page cap. A
  failed or malformed reply stops only its batch: every unfinished
  component gets a fixed reason, other results are kept, and HTTP framing
  errors are recorded instead of raised.
- Add --exclude NAME (a trailing * matches a prefix),
  --exclude-private-origins and --dry-run, which lists the identities it
  would send and runs offline.
- Query MCP servers that carry an exact pkg:pypi or pkg:npm package URL
  reconciled with their launcher package and pinned version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: offline commands run on untrusted checkouts and must not
reach the network or start processes.

Abuse case: a refactor drops an entry from a guard table, or a collector
gains a network call in a branch that only direct unit tests reach, and
the suite stays green. Code run in a subinterpreter sees neither the audit
hook nor the patched entry points.

- Parametrize the guard tests over _SOCKET_METHODS, _RESOLVER_FUNCTIONS,
  _CONNECTION_FUNCTIONS, _PROCESS_FUNCTIONS and _AUDIT_EVENTS, keep the
  documented minimum of each table, and check that the documentation names
  every entry.
- Cover connect on a Unix-domain socket, a socket created before the guard,
  every process entry point in a child interpreter, and collectors called
  directly on hostile input.
- Refuse subinterpreter creation while offline. Raising from the
  cpython.PyInterpreterState_New audit event crashes CPython 3.12, so the
  creation functions are replaced instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
The runtime gateway admits tool calls on evidence the mediated agent
presents: snapshots, trust statuses, approvals and the companion BOM.
Several checks were weaker than the contracts promised.

- The snapshot high-water mark moved only when a call was admitted, so
  an agent that kept presenting serial N after the operator issued N+1
  kept grants the operator had removed until N expired (up to 24 hours).
  An authentic newer snapshot now becomes current as soon as it is
  presented, even in a denied call, or registered with
  register_snapshot, and lower serials are denied.
- Approval quorums counted keys under one actor label, so one person
  with two enrolled keys met a two-person rule. They now count distinct
  approvers, and the approval's actor must be one of them.
- Any ring key could sign for any tenant and one key could serve every
  purpose. SignerScope limits keys to tenants, deployments, agents,
  tools and a validity period; overlapping rings are refused.
- A missing state file silently became an empty store, reopening
  approval replay and rollback after a restart. Stores are created
  explicitly, never recreated, and created owner-only without races.
- Input schemas were read as 2020-12 with Python regex semantics, so a
  value with a trailing newline passed an anchored pattern and draft-07
  dependencies were ignored. Declared dialects and ECMA-262 patterns
  are honoured.
- Endpoint revocations were free text that a typo made ineffective.
  Endpoint identities now follow one grammar.
- Standard MCP descriptor fields (title, outputSchema, _meta) denied
  every call; they are accepted when the approved digest covers them.
- Worker start-up shared the one-second evaluation budget, so slow
  hosts denied every call; start-up now has its own budget.
- Arguments were rewritten before execution in ways the approval did
  not bind; the executor now receives them as sent.

Every denial raises AuthorizationError with a stable reason code, each
denial path has a test that asserts its code, CLI errors are specific
and never tracebacks, sign refuses contracts that can never verify,
and runtime digest and runtime key-id compute what approvals bind.
on_decision records every outcome without argument values. Known-answer
vectors pin the signed wire format, and the new contract reference
specifies it. invoke_pinned_stdio keeps the whole gateway configuration
through bind_adapters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
The runtime guide is what an operator follows to put tool calls behind
the gateway. It showed one key named approval.pem signing a snapshot,
gave no way to compute the digests contracts bind, did not say that a
superseded snapshot stays usable until it is registered or revoked,
that the evidence must come from an operator channel rather than the
mediated agent, or that one-use and rollback guarantees hold per state
file. Operators were left with shortcuts such as one key for every
purpose and autonomous grants.

The guide now uses one key per purpose with encrypted private keys,
runtime digest and runtime key-id, explicit state initialization,
snapshot registration, approval quorums by person, signer scopes, the
decision callback, the worker's start-up budget, schema dialects and
the argument number domain, and links the contract reference. The
documentation hub and the README command table point to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…red floors

The published dependency ranges are what PyPI and pre-commit users install, but
CI only ever ran the single hand-maintained lock: the declared floors were never
installed (the suite could not run on the click 8.1 floor), Dependabot widened
ranges with every check green, and the Action lock was a hand copy of CI-lock
blocks. A range change or a stale copy could ship an untested or unreviewed
dependency set to every consumer.

- scripts/check_locks.py (`make lock`, `make lock-upgrade`, `make lock-check`)
  regenerates requirements-ci.lock, requirements-action.lock and the new
  requirements-floor.lock with a pinned uv and a recorded upload cut-off (a week
  of cooldown on upgrades), and checks offline that each lock was generated from
  the current declarations, that every pin is an exact hashed version inside its
  range, that the Action lock repeats the CI lock's versions and hashes without
  test tools, and that the floor lock tests exactly each declared lower bound.
  A Dependabot range change now fails until the locks are regenerated.
- Floors raised to what installs and is tested together: click 8.2.1 (separate
  stderr capture), pydantic 2.12 (required by the sigstore extra), packaging
  24.2 and jsonschema 4.25 (required by the build backend and the CycloneDX
  extra), hatchling 1.27 (PEP 639 licence files, core metadata 2.4) for the
  build system as well as the dev group.
- rfc8785 is held to 0.1.x because it defines every tool fingerprint;
  cryptography may move two majors ahead, since only Ed25519 and serialization
  are used and the newest releases are now tested on a schedule.
- The no-op yaml extra is gone, `all` refers to the other extras, development
  tools move to a PEP 735 dependency group so they are no longer published as
  an extra, and the directly imported sigstore-models is declared.
- Every lock install uses --only-binary=:all: and pip check; Dependabot waits
  seven days before proposing a release.

The regenerated CI and Action locks keep every reviewed version and hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…the parsers

The suite read whatever the developer or runner exported (a source-binding key,
an operating mode, GITHUB_* variables), had no per-test time limit, let a test
that started skipping stay green, verified the pip and BuildKit emulation only
with hand-picked examples, and judged the security modules by a global
coverage average. A hostile manifest line that the collector reads differently
from pip, or an untested branch in a confinement or signature module, could
therefore pass every gate unnoticed.

- tests/conftest.py removes the variables the tool, the Action runner and the
  gate scripts read, gives HOME and the XDG directories a private temporary
  directory, fails a test that leaves an operating mode enforced, restores the
  scanner registry, fails a test that exceeds its time limit with its traceback
  (SIGALRM, no new dependency), lists every skip, and with --strict-skips fails
  the run on any skip other than a host file-system difference. The wheel
  acceptance run uses the same conftest. tests/support.py shares load_script.
- tests/test_parser_properties.py checks the requirements collector against
  packaging however a line is decorated, that an unparseable AI requirement or
  an undecodable file leaves a gap, Dockerfile FROM and stage handling, that a
  planted secret never survives redaction (known values, URLs, assignments,
  authorization headers, tool descriptors), that no collected file kind can
  crash `scan`, and that hostile inputs parse in bounded time. The default
  Hypothesis profile is deterministic; a `fuzz` profile explores further.
- scripts/check_coverage.py adds floors for the confinement, hashing, crypto,
  signature-policy, validator, differ, offline-guard, runtime, policy,
  redaction and terminal modules, fails when one is not measured, ratchets the
  package floors to 91%/83%, and prints the measured figures to the job
  summary instead of documents quoting stale numbers.
- First-generation CLI, integration, validator and policy tests now assert the
  exact verdict, reason or change set their names promise; dead importorskip
  guards on required dependencies are plain imports; a CycloneDX library
  without 1.7 support fails instead of skipping the strict conformance tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…orkflow integrity

Verified CI is produced by the pull request's own workflow files, and
check_workflows.py only checked pins and shell expressions in the top-level
files: a change could empty the dependency-audit or CodeQL job while keeping
its name, add a pull_request_target workflow, persist checkout credentials or
reach unpinned actions through a local action. The advisory gate audited only
the Python 3.12 Linux environment (never the 3.11- and Windows-only pins, but
the runner's own pip), had no exception path, and ran only on pushes.

- ci.yml: a gating `floor` job runs the full suite on requirements-floor.lock
  on Python 3.11; every job installs with --only-binary=:all: and pip check
  and runs with --strict-skips; quality checks the locks; the distribution job
  runs twine check --strict and scripts/check_installed_wheel.py, which scans
  the demo agent with a TypeScript MCP server in every export format and
  scores it on freshly resolved dependencies (the release compatibility job
  runs the same check from the candidate's sdist); CI runs on pull requests to
  main only; runners are pinned to ubuntu-24.04.
- check_dependency_advisories.py --lock audits every pin of the named locks
  whatever its marker, lists runner-only distributions without auditing them,
  and honours reviewed waivers in config/dependency-advisory-waivers.json only
  for one advisory of one exact version, with a reason and an expiry at most
  90 days ahead; expired, malformed or stale waivers fail.
- dependencies.yml audits the locks daily and the floor lock separately, runs
  the suite weekly and on dependency changes against the newest releases the
  ranges admit, and runs the randomized property tests weekly; platforms.yml
  runs the case-insensitive and confinement branches on macOS (non-gating);
  scorecard.yml publishes OpenSSF Scorecard results; scheduled CodeQL also
  analyzes the workflows with the actions queries.
- check_workflows.py follows local actions, rejects privileged triggers,
  writable top-level permissions, jobs without timeouts and persisted checkout
  credentials, requires a full version comment that agrees for each pinned
  commit and is not older than the action's floor, forbids continue-on-error
  in ci.yml and release.yml, and requires each gate job to keep its command.
  action.yml's CodeQL pin comment names its full version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…imits

Trust boundary: the documented guarantees that operators rely on when they
gate on scan, validate and diff.

The known limitations, README rows and security model still said that
Claude Code hooks, deny rules, skill scripts, subagents, rule directories and
most invisible characters were not recorded, that symlinks are refused, and
that a lone surrogate in rag_config.json breaks exports. They now state what
is collected, which values only the binding key binds, how case variants
and in-root symlinks are handled, and what stays uncovered (nested
instruction files, skill front matter, Codex MCP servers). The binding guide
documents the a2a-card-structure/v3 profile, the RAG and Claude Code settings
profiles, the authentication rules and how to generate a binding key. The
demo walkthrough lists the two low completeness findings in its score.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…tings

0.1.0 reached PyPI 100 minutes after a 6,443-line merge; the 24-hour soak that
stands in for a second reviewer was a convention, the signed tag came a day
after publication, and the preflight could not see the release-tag ruleset or
the environments' administrator bypass that the README's verified install
relies on. Its CI evidence was the single newest run for the SHA, so anyone
could block publication by opening a pull request at main's head commit.

- check_repository_controls.py selects the newest trusted run (push or
  dispatch of ci.yml on the branch in this repository) from the whole listing,
  so a pull-request or fork run cannot displace it, while a newer trusted
  failure still wins over an older success.
- With --publish it requires an active, unbypassable ruleset stopping
  refs/tags/v* from being moved or deleted, and an environment with a required
  reviewer, administrator bypass off and deployments limited to main. With
  --publish pypi it requires the commit to have been on main for 24 hours
  (from the later of its commit time and its first push run), unless the new
  soak_override input states a reason, which is printed reduced to plain prose,
  and requires the signed, annotated v<version> tag on that commit with a
  signature GitHub reports as verified.
- release.yml passes --publish, renames the step to what it checks, audits the
  locks rather than the environment, runs the suite with --strict-skips, keeps
  release artifacts for 90 days, and writes the environment SBOM against
  requirements-action.lock: every installed distribution must be locked at its
  version, unrepresentable records fail instead of vanishing, and the legacy
  licence field is used when it is a name.
- The full protection profile requires code-owner review, and CODEOWNERS says
  what it does and names the gate paths. docs/releasing.md tags before
  publishing, documents the GitHub release and evidence attachment, a release
  freeze and the post-release records, and keeps a dated release record;
  docs/repository-controls.md describes the new checks, the advisory waivers,
  workflow integrity, Scorecard and a leased branch-cleanup runbook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
claude and others added 4 commits November 11, 2026 00:14
…emote uv scripts, trailing dots and installers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
The known-limitations catalogue now describes the merged launcher
classifier: text a shell or cmd reads again must read as written, package
installs and remote uv scripts are gaps, environment that moves a
runner's configuration withholds the identity, and token-shaped names are
redacted. It also states the limits that remain: a runner named by a
relative path or shadowed by a Windows file in the launch directory is
still classified by its name, and uvx -w and a bare pwsh script are gaps.
The Unreleased changelog lists the final-review fixes and the dropped
rich dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…ment-dependent failures

- Python 3.14 limits recursion by the C stack it actually has, so on a
  runner with a large stack json.loads accepted 100,000 levels of
  nesting and the runtime commands failed later with
  json-out-of-domain instead of json-invalid. strict_json now refuses
  input nested more than 256 levels with an explicit, non-recursive
  check, the same on every interpreter and stack size (reproduced on
  3.14.6 with an unlimited stack).
- The waiver-expiry test computed its dates when the tests were
  collected and compared them with the date when the test ran, so a run
  that crossed midnight UTC turned "91 days" into the 90-day limit. Both
  now come from one day.
- The "newest releases in the declared ranges" jobs ran the locked_only
  test that checks the Action lock's closure, which differs from the
  newest releases by design; they now deselect locked_only tests as the
  floor job does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…cher/resolver pinning, and SSE/redaction/JSONC guards
@aisecnomad aisecnomad added bug Something isn't working security Security-related changes testing Test infrastructure and coverage spec Specification and policy definitions scanner Scanner module changes python Pull requests that update python code labels Nov 12, 2026
…with #131 to #138

The branch was based on the pre-squash history of #130. Its change is re-applied on
current main; conflicts with later fixes on main are resolved in favour of main where
main already covers the same problem.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
@aisecnomad
aisecnomad changed the base branch from claude/audit-remediation to main November 12, 2026 20:02

Copy link
Copy Markdown
Collaborator Author

Reconciled with main (after #131–#138) and retargeted to main.

Re-applied on current main as a merge commit. Conflicts and how they were resolved:

  • utils/model_formats.py: kept main's text-based chat-template check (it decodes string escapes) and added this PR's detection of a dunder name joined from two string literals.
  • utils/secrets.py: kept main's token shapes and identifier mode, added hf_ tokens and this PR's raw Authorization and cookie header redaction.
  • scanners/mcp_launch.py: imports only. One behavioural clash: the new package-entry check ran before main's check for a second launch hidden behind an expansion and replaced its more specific reason, so the embedded-launch reason is now evaluated first. Both fail closed.

Checked locally: ruff, format, mypy strict, and 2,370 confinement, launcher, redaction, policy and live-MCP tests. CI on this head is the full check.

Review note: utils/confined.py is the read boundary every collector uses; the exact-spelling checks add directory listings on case-folding file systems and deserve a careful human read.

Posted by Claude Code on behalf of the maintainer; not an independent human review.


Generated by Claude Code

Comment thread tests/test_prompt_imports.py Fixed
Comment thread tests/test_prompt_imports.py Fixed
Comment thread tests/test_prompt_imports.py Fixed
…st helper

The helper created its file readable by every user; nothing reads it as
another user, so it is now owner-only. The two bare exception handlers
say why they ignore the error.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
…fails

An open that succeeded was left open when listing its directory raised, as
it does for a directory that can be searched but not read.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB

Copy link
Copy Markdown
Collaborator Author

Updated onto main after #139 and added two small commits found in review:

  • CodeQL alerts resolved. The long-path test helper created its file world-readable (high) and two exception handlers ignored an error without saying why (notes).
  • Descriptor leak closed. _open_dir_part and _open_leaf_no_follow left a successfully opened descriptor open when the exact-spelling check itself raised, as it does for a directory that can be searched but not listed. A regression test fails without the fix.

One observation left as is: _has_exact_name lists the whole directory on a case-insensitive file system (and for names with no cased letters), and that listing is not charged to the entry budget. It is linear in the directory size and cached per directory, so it is a cost note rather than a defect.

The earlier floor-job failure on this branch was the timing-ratio test test_the_analysis_of_a_large_module_is_linear (8.6 against a limit of 8), which this change does not touch; #142 makes that measurement steadier.


Generated by Claude Code

The same handling as before, written in each function so that a reader, and
the code scanner, can see every path that closes it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
@aisecnomad
aisecnomad merged commit 0990362 into main Nov 12, 2026
19 checks passed
aisecnomad pushed a commit that referenced this pull request Nov 12, 2026
aisecnomad added a commit that referenced this pull request Nov 12, 2026
…L encoding & strict I-JSON (#141)

Native inventories are parsed as strict I-JSON from bytes, with byte-order marks refused, and are not written with values a reader would reject. An added component is rated by its own findings, a change to an unsafe serialization is critical, and a ShadowScan report format or status that appears or disappears is reported. Package URLs agree with a component only for the registry it was resolved from, OCI references with a tag and a digest count as pinned while single-number tags do not, and model findings derived from recorded facts reach SARIF and VEX.

Reconciled with main after #131 to #140. The stricter timestamp validation was left out so that inventories written by earlier builds still load.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
aisecnomad pushed a commit that referenced this pull request Nov 12, 2026
…ling

Confined reads require the spelling a directory holds since #140, so a policy
entry in another case is refused where file names fold case, as it always was
where they do not. The test that expected the case variant to be accepted ran
only on such file systems and has failed on macOS since then; it now states
the refusal and runs everywhere.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
aisecnomad added a commit that referenced this pull request Nov 12, 2026
…t hermeticity & 0.2.0 alignment (#142)

The release verification step fails on each unmet condition, the macOS smoke job runs on pull requests with the confinement and collector tests, the Action refuses a subset of collectors while validation is on, and the test suite blocks outbound connections. The version is 0.2.0 in the package metadata, changelog and citation file, with the changelog entries for #139 to #142.

Reconciled with main after #131 to #141. Review added: the documented example digests are regenerated for the new version, the security policy and tutorial match the new gate-protection guidance, and a signer policy entry in another case is refused on every file system, which repairs the macOS job that had failed on main since #140.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
aisecnomad added a commit that referenced this pull request Nov 13, 2026
- Deno import-map fallback arrays are now recorded as redirect surfaces
- Legacy pre-0.2.0 tool fingerprints validate without hash_mismatch
- CLAUDE.md no longer lists nonexistent analysis_cli.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working python Pull requests that update python code scanner Scanner module changes security Security-related changes spec Specification and policy definitions testing Test infrastructure and coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants